The hard ways

解析 JSON 的成本

今天看到大神 mathias 的推,说如果程序中使用 JSON 格式来保存大量配置信息的时候,可以通过形如 JSON.parse('{"foo":42,"bar":1337}'); 的方式来加速程序的启动

我了解后发现,v8 为了加速对 JS 程序的解析,将 One-pass parsing 拆分为了 Preparsing 和 Lazy parsingv8 会将函数的函数体编译为字节码,而在 Preparsing 中,不会将所有函数的函数体都一股脑地编译为字节码,而是将对非 PIFE 的函数的函数体的编译动作延迟到程序中第一次使用它们的时候,这就是 Lazy parsing

虽然 Preparsing 中不会对函数体进一步处理,它还是会扫描整个 JS 文件,保证函数体中的内容是符合语法定义的所以如果在程序中直接使用对象字面量来存放配置信息,比如:

function start() {
  let cfg = {
    foo: 42,
    bar: 1337
  };
  // consume cfg
}
start()

会对对象字面量进行两次解析,一次发生在 Preparsing 中,目的是为了保证其语法正确性;另一次发生在 Lazy parsing 中,因为 start 函数被调用了,于是需要将其函数体进行编译,此时会解析第二次

而如果将程序改成:

function start() {
  let cfg = JSON.parse('{"foo":42,"bar":1337}');
  // consume cfg
}
start()

我们直接使用了对象字面量对应的字符串形式,然后使用 JSON.parse 将对其的解析放到了运行时,这就使得 Preparsing 不需要解析对象字面量,取而代之的是解析对象字面量对应的字符串,因此可以加速解析过程,最终会加速程序的启动

不建议大家直接使用这一方式进行优化,可以对自己的项目使用此优化方式前后的性能差异来做最终的决定另一方面,使用这样的方式,会导致 IDE 无法直接感知 cfg 中的内容,所以最好看看有没有 babel 插件可以用或者自制一个也是相当简单的

回想到垠神的 对 Parser 的误解 一文中,有这样的描述:

很多人写 parser,很在乎所谓的“one-pass parser”他们试图扫描一遍代码文本就构造出最终的 AST 结构可是如果你放松这个条件,允许用多 pass 的parser,就会容易很多你可以在第一遍用很容易的办法构造一个粗略的树结构,然后再写一个递归树遍历过程,把某些在第一遍的时候没法确定的结构进行小规模的转换,最后得到正确的 AST

想要一遍就 parse 出最终的 AST,可以说是一种过早优化(premature optimization)有些人盲目地认为只扫描一遍代码,会比扫描两遍要快一些然而由于你必须在这一遍扫描里进行多度复杂的操作,最终的性能也许还不如很快的扫完第一遍,然后再很快的遍历转换由此生成的树结构

v8 中这样的优化方式,刚好可以作为垠神描述的例证

    Made with gadget